/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.watchface

import android.opengl.GLES20
import android.opengl.GLU
import android.opengl.GLUtils
import android.util.Log
import androidx.annotation.Px
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer

/** Whether to check for GL errors. This is slow, so not appropriate for production builds. */
internal const val CHECK_GL_ERRORS = false

private const val TAG = "RenderBufferTexture"

/**
 * Checks if any of the GL calls since the last time this method was called set an error condition.
 * Call this method immediately after calling a GL method. Pass the name of the GL operation. For
 * example:
 * <pre>
 * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
 * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
 *
 * If the operation is not successful, the check throws an exception.
 *
 * *Note* This is quite slow so it's best to use it sparingly in production builds.
 *
 * @param glOperation name of the OpenGL call to check
 */
internal fun checkGlError(glOperation: String) {
    val error = GLES20.glGetError()
    if (error != GLES20.GL_NO_ERROR) {
        var errorString = GLU.gluErrorString(error)
        if (errorString == null) {
            errorString = GLUtils.getEGLErrorString(error)
        }
        val message =
            glOperation + " caused GL error 0x" + Integer.toHexString(error) + ": " + errorString
        Log.e(TAG, message)
        throw RuntimeException(message)
    }
}

/**
 * Handles a framebuffer and texture for rendering to texture. Also handles drawing a full screen
 * quad to apply the texture as an overlay.
 */
internal class RenderBufferTexture(@Px private val width: Int, @Px private val height: Int) {

    val framebuffer = IntArray(1)
    val textureId = IntArray(1)

    val fullScreenQuad =
        Gles2TexturedTriangleList(
            Gles2TexturedTriangleList.Program(),
            // List of (x,y,z) coordinates for two triangles to make a quad that covers the whole
            // screen
            floatArrayOf(
                -1.0f,
                -1.0f,
                0.5f,
                -1.0f,
                1.0f,
                0.5f,
                1.0f,
                -1.0f,
                0.5f,
                -1.0f,
                1.0f,
                0.5f,
                1.0f,
                -1.0f,
                0.5f,
                1.0f,
                1.0f,
                0.5f
            ),

            // List of (u, v) texture coordinates.
            floatArrayOf(0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f)
        )

    init {
        // Create the texture
        GLES20.glGenTextures(1, textureId, 0)
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0])
        GLES20.glTexParameteri(
            GLES20.GL_TEXTURE_2D,
            GLES20.GL_TEXTURE_WRAP_S,
            GLES20.GL_CLAMP_TO_EDGE
        )
        GLES20.glTexParameteri(
            GLES20.GL_TEXTURE_2D,
            GLES20.GL_TEXTURE_WRAP_T,
            GLES20.GL_CLAMP_TO_EDGE
        )
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)

        GLES20.glTexImage2D(
            GLES20.GL_TEXTURE_2D,
            0,
            GLES20.GL_RGBA,
            width,
            height,
            0,
            GLES20.GL_RGBA,
            GLES20.GL_UNSIGNED_BYTE,
            null
        )
        if (CHECK_GL_ERRORS) {
            checkGlError("glTexImage2D")
        }

        // Create the frame buffer
        GLES20.glGenFramebuffers(1, framebuffer, 0)
        if (CHECK_GL_ERRORS) {
            checkGlError("glGenFramebuffers")
        }
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer[0])
        if (CHECK_GL_ERRORS) {
            checkGlError("glBindFramebuffer")
        }
        GLES20.glFramebufferTexture2D(
            GLES20.GL_FRAMEBUFFER,
            GLES20.GL_COLOR_ATTACHMENT0,
            GLES20.GL_TEXTURE_2D,
            textureId[0],
            0
        )
        if (CHECK_GL_ERRORS) {
            checkGlError("glFramebufferTexture2D")
        }

        val status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)
        if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
            throw RuntimeException("Failed to create framebuffer")
        }

        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
    }

    fun bindFrameBuffer() {
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer[0])
        if (CHECK_GL_ERRORS) {
            checkGlError("glBindFramebuffer")
        }
        GLES20.glViewport(0, 0, width, height)
        if (CHECK_GL_ERRORS) {
            checkGlError("glFramebufferTexture2D")
        }
    }

    fun compositeQuad() {
        fullScreenQuad.program.bindProgramAndAttribs()
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0])
        if (CHECK_GL_ERRORS) {
            checkGlError("glBindTexture")
        }
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
        if (CHECK_GL_ERRORS) {
            checkGlError("glBlendFunc")
        }
        fullScreenQuad.draw()
    }
}

/** A list of triangles drawn with a texture using OpenGL ES 2.0. */
internal class Gles2TexturedTriangleList(
    internal val program: Program,
    triangleCoords: FloatArray,
    private val textureCoords: FloatArray
) {
    init {
        require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
            ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
        }
        require(textureCoords.size % (VERTICES_PER_TRIANGLE * TEXTURE_COORDS_PER_VERTEX) == 0) {
            ("must be multiple of VERTICES_PER_TRIANGLE * NUM_TEXTURE_COMPONENTS texture " +
                "coordinates")
        }
    }

    /** The VBO containing the vertex coordinates. */
    private val vertexBuffer =
        ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
            .apply { order(ByteOrder.nativeOrder()) }
            .asFloatBuffer()
            .apply {
                put(triangleCoords)
                position(0)
            }

    /** The VBO containing the vertex coordinates. */
    private val textureCoordsBuffer =
        ByteBuffer.allocateDirect(textureCoords.size * BYTES_PER_FLOAT)
            .apply { order(ByteOrder.nativeOrder()) }
            .asFloatBuffer()
            .apply {
                put(textureCoords)
                position(0)
            }

    /** Number of coordinates in this triangle list. */
    private val numCoords = triangleCoords.size / COORDS_PER_VERTEX

    /** Draws this triangle list using OpenGL commands. */
    internal fun draw() {
        // Pass vertex data, and texture coordinates to OpenGL.
        program.bind(vertexBuffer, textureCoordsBuffer)

        // Draw the triangle list.
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
        if (CHECK_GL_ERRORS) checkGlError("glDrawArrays")

        program.unbindAttribs()
    }

    /** OpenGL shaders for drawing textured triangle lists. */
    internal class Program {
        /** ID OpenGL uses to identify this program. */
        private val programId: Int

        /** Handle for aPosition attribute in vertex shader. */
        private val positionHandle: Int

        /** Handle for aTextureCoordinate uniform in fragment shader. */
        private val textureCoordinateHandle: Int

        companion object {
            /** Trivial pass through vertex shader. */
            private const val VERTEX_SHADER_CODE =
                "" +
                    "attribute vec4 aPosition;\n" +
                    "attribute vec4 aTextureCoordinate;\n" +
                    "varying vec2 textureCoordinate;\n" +
                    "void main() {\n" +
                    "    gl_Position = aPosition;\n" +
                    "    textureCoordinate = aTextureCoordinate.xy;\n" +
                    "}\n"

            /** Trivial fragment shader that draws with a texture. */
            private const val FRAGMENT_SHADER_CODE =
                "" +
                    "varying highp vec2 textureCoordinate;\n" +
                    "uniform sampler2D texture;\n" +
                    "void main() {\n" +
                    "    gl_FragColor = texture2D(texture, textureCoordinate);\n" +
                    "}\n"
        }

        /**
         * Tells OpenGL to use this program. Call this method before drawing a sequence of triangle
         * lists.
         */
        fun bindProgramAndAttribs() {
            GLES20.glUseProgram(programId)
            if (CHECK_GL_ERRORS) {
                checkGlError("glUseProgram")
            }

            // Enable vertex array (VBO).
            GLES20.glEnableVertexAttribArray(positionHandle)
            if (CHECK_GL_ERRORS) {
                checkGlError("glEnableVertexAttribArray")
            }

            GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
            if (CHECK_GL_ERRORS) {
                checkGlError("glEnableVertexAttribArray")
            }
        }

        fun unbindAttribs() {
            GLES20.glDisableVertexAttribArray(positionHandle)
            if (CHECK_GL_ERRORS) {
                checkGlError("glDisableVertexAttribArray")
            }

            GLES20.glDisableVertexAttribArray(textureCoordinateHandle)
            if (CHECK_GL_ERRORS) {
                checkGlError("glDisableVertexAttribArray")
            }
        }

        /** Sends the given MVP matrix, vertex data, and color to OpenGL. */
        fun bind(vertexBuffer: FloatBuffer?, textureCoordinatesBuffer: FloatBuffer?) {
            // Pass the VBO with the triangle list's vertices to OpenGL.
            GLES20.glVertexAttribPointer(
                positionHandle,
                COORDS_PER_VERTEX,
                GLES20.GL_FLOAT,
                false /* normalized */,
                VERTEX_STRIDE,
                vertexBuffer
            )
            if (CHECK_GL_ERRORS) {
                checkGlError("glVertexAttribPointer")
            }

            // Pass the VBO with the triangle list's texture coordinates to OpenGL.
            GLES20.glVertexAttribPointer(
                textureCoordinateHandle,
                TEXTURE_COORDS_PER_VERTEX,
                GLES20.GL_FLOAT,
                false /* normalized */,
                TEXTURE_COORDS_VERTEX_STRIDE,
                textureCoordinatesBuffer
            )
            if (CHECK_GL_ERRORS) {
                checkGlError("glVertexAttribPointer")
            }
        }

        /**
         * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
         * should be used for all triangle lists being drawn.
         */
        init {
            // Prepare shaders.
            val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE)
            val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE)

            // Create empty OpenGL Program.
            programId = GLES20.glCreateProgram()
            if (CHECK_GL_ERRORS) checkGlError("glCreateProgram")
            check(programId != 0) { "glCreateProgram failed" }

            // Add the shaders to the program.
            GLES20.glAttachShader(programId, vertexShader)
            if (CHECK_GL_ERRORS) {
                checkGlError("glAttachShader")
            }
            GLES20.glAttachShader(programId, fragmentShader)

            // Link the program so it can be executed.
            GLES20.glLinkProgram(programId)
            if (CHECK_GL_ERRORS) {
                checkGlError("glLinkProgram")
            }

            // Get a handle to the vertex shader's aPosition attribute.
            positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
            if (CHECK_GL_ERRORS) {
                checkGlError("glGetAttribLocation positionHandle")
            }

            // Get a handle to vertex shader's aUV attribute.
            textureCoordinateHandle = GLES20.glGetAttribLocation(programId, "aTextureCoordinate")
            if (CHECK_GL_ERRORS) {
                checkGlError("glGetAttribLocation textureCoordinateHandle")
            }

            // Enable vertex array (VBO).
            GLES20.glEnableVertexAttribArray(positionHandle)
            if (CHECK_GL_ERRORS) {
                checkGlError("glEnableVertexAttribArray")
            }
        }
    }

    internal companion object {
        /** Number of coordinates per vertex in this array: one for each of x, y, and z. */
        private const val COORDS_PER_VERTEX = 3

        /** Number of texture coordinates per vertex in this array: one for u & v */
        private const val TEXTURE_COORDS_PER_VERTEX = 2

        /** Number of bytes to store a float in GL. */
        const val BYTES_PER_FLOAT = 4

        /** Number of bytes per vertex. */
        private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT

        /** Number of bytes per vertex for texture coords. */
        private const val TEXTURE_COORDS_VERTEX_STRIDE = TEXTURE_COORDS_PER_VERTEX * BYTES_PER_FLOAT

        /** Triangles have three vertices. */
        private const val VERTICES_PER_TRIANGLE = 3

        /**
         * Compiles an OpenGL shader.
         *
         * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
         * @param shaderCode string containing the shader source code
         * @return ID for the shader
         */
        internal fun loadShader(type: Int, shaderCode: String): Int {
            // Create a vertex or fragment shader.
            val shader = GLES20.glCreateShader(type)
            if (CHECK_GL_ERRORS) checkGlError("glCreateShader")
            check(shader != 0) { "glCreateShader failed" }

            // Add the source code to the shader and compile it.
            GLES20.glShaderSource(shader, shaderCode)
            if (CHECK_GL_ERRORS) checkGlError("glShaderSource")
            GLES20.glCompileShader(shader)
            if (CHECK_GL_ERRORS) checkGlError("glCompileShader")
            return shader
        }
    }
}
